package classloader;

import classloader.module.ModuleBase;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

/**
 * This class will load the specified Module. Modules are loaded this way
 * because then we're able to unload it from the memory as well, without
 * killing the JVM. This functionality can be used for... well anything really.
 */
public class ModuleClassLoader extends ClassLoader {
	
	/**
	 * In here the (only!) pointer to the loaded module will be stored. Note 
	 * that when you store a pointer to the module somewhere in your own
	 * modules, unloading this module will not work anymore.
	 */
	private ModuleBase m_pModule = null;
	
	/**
	 * The constructor will load up the classloader and immediately the
	 * specified as well. After this, the getModule() can be used to retrieve a
	 * pointer to the loaded module. To unload this module, simply set the 
	 * variable holding the pointer to this ClassLoader to null.
	 */
	public ModuleClassLoader (String sModuleName) throws ClassNotFoundException {
		Class <?> pModuleClass = loadClass ("classloader.module." + sModuleName);
		
		if (pModuleClass == null) {
			throw new ClassNotFoundException ("Class object is null.");
		}
		
		try {
			m_pModule = (ModuleBase) pModuleClass.newInstance ();
		} catch (Exception pException) {
			// If this happens, the module that's being loaded is not a proper
			// module.
			
		}
	}
	
	/**
	 * Returns the pointer to the loaded Module. Always use this. Never store
	 * the pointer.
	 */
	public ModuleBase getModule () {
		return m_pModule;
	}
	
	/**
	 * This method will try to load the specified class. Any classes not in the
	 * package classloader.module will be loaded using the system classloader.
	 * This also applies to the ModuleBase abstract class in the module package,
	 * because otherwise we can't cast the loaded class to ModuleBase.
	 */
	public Class <?> loadClass (String sName) throws ClassNotFoundException {
		System.out.println(sName);
		
		if (!sName.startsWith ("classloader.module.") || sName.equals ("classloader.module.ModuleBase")) {
			return ClassLoader.getSystemClassLoader ().loadClass (sName);
		}
		
		String sClassPath = sName.replace('.', File.separatorChar) + ".class";
		System.out.println("Loading " + sClassPath);
		
		byte [] aBytes;
		try {
			aBytes = loadClassData (sClassPath);
		} catch (Exception pException) {
			throw new ClassNotFoundException (pException.getMessage (), pException);
		}
		
		try {
			return defineClass (sName, aBytes);
		} catch (Exception pException) {
			throw new ClassNotFoundException (pException.getMessage (), pException);
		}
	}
	
	/**
	 * This method will define the class from the provided bytecode.
	 */
	protected Class <?> defineClass (String sName, byte [] aBytes) {
		return defineClass (sName, aBytes, 0, aBytes.length, null);
	}
	
	/**
	 * This method reads the classfile of a compiled Java class and returns the
	 * read bytes in an array. This array can then be used to define the class.
	 */
	protected byte [] loadClassData (String sClassPath) throws FileNotFoundException, IOException {
		File pClassFile = new File (sClassPath);
		if (!pClassFile.exists ()) {
			throw new FileNotFoundException (sClassPath);
		}
		
		int nFileSize = (int) pClassFile.length ();
		byte [] aBytes = new byte [nFileSize];
		
		DataInputStream pInput = new DataInputStream (new FileInputStream (pClassFile));
		
		pInput.readFully (aBytes);
		pInput.close ();
		
		return aBytes;
	}
}